package commDao

import (
	"fmt"
	"github.com/TarsCloud/TarsGo/tars"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"reflect"
	"runtime/debug"
	"strconv"
	"strings"
	"xxgame.com/CommApp/Util/goroutine"
	"xxgame.com/CommApp/Util/littleFunc"
	"xxgame.com/CommApp/Util/log"
)

type CommEntityIF interface {
	TableName() string
}

// 获取object标签（主要用于LockMgr对象锁获取以及PstObjMgr临时缓存对象获取）
//	返回说明：
// 		容器标签string、子项标签string
/*
	可能出bug的情况一：
		例如：userHeroInfoEntity.UserHeroInfoEntity 这个实例，有使用场景是通过Uid和Type
			去查询出对应的主角英雄，但是如果按照下述算法，Type的查询条件是被过滤掉，所以构成的 tag1 等于 SUB_uid_tblName
			但是另外一个地方可能通过 Uid和HeroID 去查询英雄，这里构成的 tag2 等于 SUB_uid_tblName_HeroID, tag1 和 tag2
			不一样，但是极有可能出现下述问题：
				1、同一个协程下：操作同一行DB数据，这样就导致同一个协程无法正常cache起来，也无法正常将同一个DB数据锁上；
				2、不同协程下：由于有容器锁的的第一层大锁保护，就不会出现1的情况；（这种情况还是可以保证数据正确）
				3、上述问题通过 GetFuzzyMatch 与 CompareEntity 方案解决
 */
func GetTag(cond CommEntityIF, obj CommEntityIF) (string, string) {
	rfCond := reflect.ValueOf(cond)
	rfUid := rfCond.Elem().FieldByName("Uid")
	if !rfUid.IsValid() {
		errMsg := fmt.Sprintf("获取对象标签错误，GetTag(), " +
			"属性Uid不存在: curGoid=%+v, cond=%+v, stack=%+v",
			goroutine.GetID(), cond, string(debug.Stack()))
		log.PST.Errorf(errMsg)
		panic(errMsg)
	}
	if 0 == rfUid.Int() {
		errMsg := fmt.Sprintf("获取对象标签错误，GetTag(), " +
			"cond没传入Uid或Uid等于0: curGoid=%+v, cond=%+v, stack=%+v",
			goroutine.GetID(), cond, string(debug.Stack()))
		log.PST.Errorf(errMsg)
		panic(errMsg)
	}
	rfMethod := rfCond.MethodByName("TableName")
	results := rfMethod.Call([]reflect.Value{})
	tblName := results[0].Interface().(string)
	tagFormats := []string{}
	keys := []interface{}{}
	keys = append(keys, "SUB", rfUid.Int(), tblName)
	tagFormats = append(tagFormats, "%+v", "%+v", "%+v")
	// 如果obj（obj是已从DB返回含数据的对象）不为空，
	// 需要获取ID属性作为tag的第四位填进去！！！！！
	if nil != obj {
		rfObj := reflect.ValueOf(obj)
		rfID := rfObj.Elem().FieldByName("ID")
		if 0 < rfID.Int() { // 非零方可做key的组成部分
			keys = append(keys, rfID.Int())
			tagFormats = append(tagFormats, "%+v")
		}
	}
	rfCondTyp := rfCond.Elem().Type()
	for i := 0; i < rfCondTyp.NumField(); i++ {
		fieldName := rfCondTyp.Field(i).Name
		if -1 != strings.LastIndex(fieldName, "ID") {
			rfVal := rfCond.Elem().Field(i)
			if rfVal.IsValid() {
				var idVal int64
				switch rfVal.Kind() {
				case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
					idVal = rfVal.Int()
				case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
					idVal = int64(rfVal.Uint())
				case reflect.String:
					idVal_t, err := strconv.Atoi(rfVal.String())
					if nil != err {
						errMsg := fmt.Sprintf("获取对象标签错误，GetTag(), " +
							"字符转数字失败: curGoid=%+v, rfVal=%+v", goroutine.GetID(), rfVal.String())
						log.PST.Errorf(errMsg)
						panic(errMsg)
					}
					idVal = int64(idVal_t)
				default:
					errMsg := fmt.Sprintf("获取对象标签错误，GetTag()，不支持属性类型：" +
						"curGoid=%+v, rfVal.Kind()=%+v", goroutine.GetID(), rfVal.Kind().String())
					log.PST.Errorf(errMsg)
					panic(errMsg)
				}
				if 0 != idVal {
					keys = append(keys, rfVal)
					tagFormats = append(tagFormats, "%+v")
				}
			}
		}
	}
	return fmt.Sprintf("%+v_%+v_%+v", "CTN", rfUid.Int(), tblName),
		fmt.Sprintf(strings.Join(tagFormats, "_"), keys...)
}

// 倘若当前通用类不满足逻辑要求，新创建的类组合或继承CommDao类，
// 如果有通用访问DB的接口逻辑，也可以在当前文件增加，避免重复代码，增加维护难度
type CommDao struct {
	db *gorm.DB
}

// 创建通用DB访问对象
func New(db *gorm.DB) *CommDao {
	return &CommDao{db}
}

// 插入数据（支持批量插入，data是实例地址切片即可）
func (this *CommDao) Add(data interface{}) error {
	rType := reflect.TypeOf(data)
	rValue := reflect.ValueOf(data)
	if reflect.Slice == rType.Kind() {
		if 0 >= rValue.Len() {
			return errors.New("len <= 0")
		}
		data_t := littleFunc.SliceTypeChange(data, []interface{}{})
		_, ok := data_t.([]interface{})[0].(CommEntityIF)
		if !ok {
			return errors.New("convert to type:CommEntityIF fail")
		}
		var val CommEntityIF
		for i := 0; i < rValue.Len(); i++ {
			val = rValue.Index(i).Interface().(CommEntityIF)
			break
		}
		return errors.WithStack(this.db.Table(val.TableName()).Create(data).Error)
	} else {
		data_t := data.(CommEntityIF)
		return errors.WithStack(this.db.Table(data_t.TableName()).Create(data).Error)
	}
}

// 删除数据
func (this *CommDao) DeleteByWhere(where CommEntityIF) (bool, error) {
	err := this.db.Table(where.TableName()).Where(where).Delete(where).Error
	if err != nil {
		if errors.Is(err, logger.ErrRecordNotFound) {
			return false, nil
		}
		return false, errors.WithStack(err)
	}
	return true, nil
}

// 更新数据（更新单个或多个字段）【更新非空非零字段，设置为0的字段无法更新】
func (this *CommDao) Updates(where CommEntityIF, upField CommEntityIF) error {
	result := this.db.Table(where.TableName()).Where(where).Updates(upField)
	if result.Error != nil {
		return errors.WithStack(result.Error)
	}
	if result.RowsAffected != 1 {
		return errors.New("Not RowsAffected")
	}
	return nil
}

// 更新实体数据（更新单个或多个字段）【更新整个实体所有字段，即使为空字段】
func (this *CommDao) Saves(where CommEntityIF, entity CommEntityIF) error {
	rfEntity := reflect.ValueOf(entity).Elem()
	priKey := rfEntity.FieldByName("ID")
	if !priKey.IsValid() {
		errMsg := fmt.Sprintf("CommDao.Saves() 传入entity无ID，" +
			"非法：tblName=%+v, where=%+v, entity=%+v", entity.TableName(),
			where, entity)
		tars.TLOG.Error(errMsg)
		panic(errMsg)
	}
	var result *gorm.DB
	if 0 < priKey.Int() {
		// 如果entity有主键prikey，则优先使用Save接口整持久化
		// 调用Save接口，必须prikey存在，例如prikey为0，则会插入没必要的新记录
		result = this.db.Table(where.TableName()).Where(where).Save(entity)
	} else {
		// 如果entity无主键prikey，则优先使用Updates接口持久化
		// 如果数据库中无该记录，执行Update会失败，错误日志会写到PST日志
		result = this.db.Table(where.TableName()).Where(where).
			Select("*").Updates(entity)
	}
	if result.Error != nil {
		return errors.WithStack(result.Error)
	}
	if result.RowsAffected != 1 {
		return errors.New("Not RowsAffected")
	}
	return nil
}

// 查找数据（条件查询）
// where: 实例对象指定的Field，传入对象地址（where为空实例xxx.xxx{},则可以相当于FindAll使用）
// list: 外部传入具体实例类型列表，传入列表地址
func (this *CommDao) FindByWhere(where CommEntityIF, list interface{}) error {
	err := this.db.Table(where.TableName()).Where(where).Find(list).Error
	if nil != err {
		return err
	}
	return nil
}

// 查询数据（单条记录）
func (this *CommDao) OneById(where CommEntityIF) (CommEntityIF, error, int) {
	ret := this.db.Table(where.TableName()).Where(where).First(where)
	//if nil != err {
	//	err = errors.WithStack(err)
	//}
	return where, ret.Error, int(ret.RowsAffected)
}

// 查询数据（单条记录）
func (this *CommDao) OneById2(where CommEntityIF, obj CommEntityIF) (CommEntityIF, error, int) {
	ret := this.db.Table(where.TableName()).Where(where).First(obj)
	//if nil != err {
	//	err = errors.WithStack(err)
	//}
	return obj, ret.Error, int(ret.RowsAffected)
}

// 查找数据（多id查询）
func (this *CommDao) FindByIds(tarObj CommEntityIF, idKeyName string, idList []int, list interface{}) error {
	err := this.db.Table(tarObj.TableName()).Where(fmt.Sprintf("%s IN (?)", idKeyName), idList).Find(list).Error
	if nil != err {
		return err
	}
	return nil
}